/*
 * File    : DiskManagerFileInfoImpl.java
 * Created : 18-Oct-2003
 * By      : Olivier
 * 
 * Azureus - a Java Bittorrent client
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details ( see the LICENSE file ).
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package org.gudy.azureus2.core3.disk.impl;

/*
 * Created on 3 juil. 2003
 *
 */
import java.io.File;
import java.io.IOException;

import org.gudy.azureus2.core3.disk.DiskManager;
import org.gudy.azureus2.core3.disk.DiskManagerFileInfo;
import org.gudy.azureus2.core3.disk.DiskManagerFileInfoListener;
import org.gudy.azureus2.core3.disk.DiskManagerFileInfoSet;
import org.gudy.azureus2.core3.disk.DiskManagerPiece;
import org.gudy.azureus2.core3.download.DownloadManager;
import org.gudy.azureus2.core3.download.DownloadManagerState;
import org.gudy.azureus2.core3.torrent.TOTorrentFile;
import org.gudy.azureus2.core3.util.Constants;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.DirectByteBuffer;
import org.gudy.azureus2.core3.util.DirectByteBufferPool;
import org.gudy.azureus2.core3.util.StringInterner;

import com.aelitis.azureus.core.diskmanager.cache.CacheFile;
import com.aelitis.azureus.core.diskmanager.cache.CacheFileManagerException;
import com.aelitis.azureus.core.diskmanager.cache.CacheFileManagerFactory;
import com.aelitis.azureus.core.diskmanager.cache.CacheFileOwner;
import com.aelitis.azureus.core.util.CopyOnWriteList;
import com.aelitis.azureus.core.util.average.AverageFactory;
import com.aelitis.azureus.core.util.average.AverageFactory.LazyMovingImmediateAverageAdapter;
import com.aelitis.azureus.core.util.average.AverageFactory.LazyMovingImmediateAverageState;

/**
 * @author Olivier
 * 
 */
public class DiskManagerFileInfoImpl implements DiskManagerFileInfo, CacheFileOwner {
    private String root_dir;
    private final File relative_file;

    private int file_index;
    private CacheFile cache_file;

    private String extension;
    private long downloaded;

    private DiskManagerHelper diskManager;
    private TOTorrentFile torrent_file;

    private int priority = 0;

    protected boolean skipped_internal = false;

    private volatile CopyOnWriteList<DiskManagerFileInfoListener> listeners; // save mem and allocate if needed later

    public DiskManagerFileInfoImpl(DiskManagerHelper _disk_manager, String _root_dir, File _relative_file, int _file_index,
            TOTorrentFile _torrent_file, int _storage_type)

    throws CacheFileManagerException {
        diskManager = _disk_manager;
        torrent_file = _torrent_file;

        root_dir = _root_dir.endsWith(File.separator) ? _root_dir : (_root_dir + File.separator);
        relative_file = _relative_file;

        file_index = _file_index;

        int cache_st = DiskManagerUtil.convertDMStorageTypeToCache(_storage_type);

        cache_file = CacheFileManagerFactory.getSingleton().createFile(this, new File(root_dir + relative_file.toString()), cache_st);

        if (cache_st == CacheFile.CT_COMPACT || cache_st == CacheFile.CT_PIECE_REORDER_COMPACT) {

            skipped_internal = true;
        }
    }

    public String getCacheFileOwnerName() {
        return (diskManager.getInternalName());
    }

    public TOTorrentFile getCacheFileTorrentFile() {
        return (torrent_file);
    }

    public File getCacheFileControlFileDir() {
        return (diskManager.getDownloadState().getStateFile());
    }

    public int getCacheMode() {
        return (diskManager.getCacheMode());
    }

    public void flushCache()

    throws Exception {
        cache_file.flushCache();
    }

    public void moveFile(String new_root_dir, File new_absolute_file, boolean link_only)

    throws CacheFileManagerException {
        if (!link_only) {

            cache_file.moveFile(new_absolute_file);
        }

        root_dir = new_root_dir.endsWith(File.separator) ? new_root_dir : (new_root_dir + File.separator);
    }

    public void renameFile(String new_name)

    throws CacheFileManagerException {
        cache_file.renameFile(new_name);
    }

    public CacheFile getCacheFile() {
        return (cache_file);
    }

    public void setAccessMode(int mode)

    throws CacheFileManagerException {
        int old_mode = cache_file.getAccessMode();

        cache_file.setAccessMode(mode == DiskManagerFileInfo.READ ? CacheFile.CF_READ : CacheFile.CF_WRITE);

        if (old_mode != mode) {

            diskManager.accessModeChanged(this, old_mode, mode);
        }
    }

    public int getAccessMode() {
        int mode = cache_file.getAccessMode();

        return (mode == CacheFile.CF_READ ? DiskManagerFileInfo.READ : DiskManagerFileInfo.WRITE);
    }

    /**
     * @return
     */
    public long getDownloaded() {
        return downloaded;
    }

    /**
     * @return
     */
    public String getExtension() {
        return extension;
    }

    /**
     * @return
     */
    public File getFile(boolean follow_link) {
        if (follow_link) {

            File res = getLink();

            if (res != null) {

                return (res);
            }
        }

        return (new File(root_dir + relative_file.toString()));
    }

    public TOTorrentFile getTorrentFile() {
        return (torrent_file);
    }

    public boolean setLink(File link_destination) {
        Debug.out("setLink: download must be stopped");

        return (false);
    }

    public boolean setLinkAtomic(File link_destination) {
        Debug.out("setLink: download must be stopped");

        return (false);
    }

    public File getLink() {
        return (diskManager.getDownloadState().getFileLink(file_index, getFile(false)));
    }

    public boolean setStorageType(int type) {
        DiskManagerFileInfoSet set = diskManager.getFileSet();
        boolean[] toSet = new boolean[set.nbFiles()];
        toSet[file_index] = true;
        return set.setStorageTypes(toSet, type)[file_index];
    }

    public int getStorageType() {
        return (DiskManagerUtil.convertDMStorageTypeFromString(diskManager.getStorageType(file_index)));
    }

    protected boolean isLinked() {
        return (getLink() != null);
    }

    /**
     * @return
     */
    public int getFirstPieceNumber() {
        return torrent_file.getFirstPieceNumber();
    }

    public int getLastPieceNumber() {
        return torrent_file.getLastPieceNumber();
    }

    /**
     * @return
     */
    public long getLength() {
        return torrent_file.getLength();
    }

    public int getIndex() {
        return (file_index);
    }

    /**
     * @return
     */
    public int getNbPieces() {
        return torrent_file.getNumberOfPieces();
    }

    /**
     * @param l
     */
    public void setDownloaded(long l) {
        downloaded = l;
    }

    /**
     * @param string
     */
    public void setExtension(String string) {
        extension = StringInterner.intern(string);
    }

    /**
     * @return
     */
    public int getPriority() {
        return priority;
    }

    /**
     * @param b
     */
    public void setPriority(int b) {
        priority = b;
        diskManager.priorityChanged(this);
    }

    /**
     * @return
     */
    public boolean isSkipped() {
        return skipped_internal;
    }

    /**
     * @param skipped
     */
    public void setSkipped(boolean _skipped) {

        int existing_st = getStorageType();

        // currently a non-skipped file must be linear

        if (!_skipped && existing_st == ST_COMPACT) {
            if (!setStorageType(ST_LINEAR)) {
                return;
            }
        }

        if (!_skipped && existing_st == ST_REORDER_COMPACT) {
            if (!setStorageType(ST_REORDER)) {
                return;
            }
        }

        setSkippedInternal(_skipped);
        diskManager.skippedFileSetChanged(this);
        if (!_skipped) {
            boolean[] toCheck = new boolean[diskManager.getFileSet().nbFiles()];
            toCheck[file_index] = true;
            DiskManagerUtil.doFileExistenceChecks(diskManager.getFileSet(), toCheck, diskManager.getDownloadState().getDownloadManager(), true);
        }
    }

    protected void setSkippedInternal(boolean _skipped) {
        skipped_internal = _skipped;

        DownloadManager dm = getDownloadManager();

        if (dm != null && !dm.isDestroyed()) {

            DownloadManagerState dm_state = diskManager.getDownloadState();

            String dnd_sf = dm_state.getAttribute(DownloadManagerState.AT_DND_SUBFOLDER);

            if (dnd_sf != null) {

                File link = getLink();

                File file = getFile(false);

                if (_skipped) {

                    if (link == null || link.equals(file)) {

                        File parent = file.getParentFile();

                        if (parent != null) {

                            File new_parent = new File(parent, dnd_sf);

                            File new_file = new File(new_parent, file.getName());

                            if (!new_file.exists()) {

                                if (!new_parent.exists()) {

                                    new_parent.mkdirs();
                                }

                                if (new_parent.canWrite()) {

                                    boolean ok;

                                    try {
                                        dm_state.setFileLink(file_index, file, new_file);

                                        cache_file.moveFile(new_file);

                                        ok = true;

                                    } catch (Throwable e) {

                                        ok = false;

                                        Debug.out(e);
                                    }

                                    if (!ok) {

                                        dm_state.setFileLink(file_index, file, link);
                                    }
                                }
                            }
                        }
                    }
                } else {

                    if (link != null && !file.exists()) {

                        File parent = file.getParentFile();

                        if (parent != null && parent.canWrite()) {

                            File new_parent = parent.getName().equals(dnd_sf) ? parent : new File(parent, dnd_sf);

                            // use link name to handle incomplete file suffix if set

                            File new_file = new File(new_parent, link.getName());

                            if (new_file.equals(link)) {

                                boolean ok;

                                try {
                                    String incomp_ext = dm_state.getAttribute(DownloadManagerState.AT_INCOMP_FILE_SUFFIX);

                                    if (incomp_ext != null && incomp_ext.length() > 0) {

                                        File new_link = new File(file.getParentFile(), file.getName() + incomp_ext);

                                        dm_state.setFileLink(file_index, file, new_link);

                                        cache_file.moveFile(new_link);

                                    } else {

                                        dm_state.setFileLink(file_index, file, null);

                                        cache_file.moveFile(file);
                                    }

                                    File[] files = new_parent.listFiles();

                                    if (files != null && files.length == 0) {

                                        new_parent.delete();
                                    }

                                    ok = true;

                                } catch (Throwable e) {

                                    ok = false;

                                    Debug.out(e);
                                }

                                if (!ok) {

                                    dm_state.setFileLink(file_index, file, link);
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    public DiskManager getDiskManager() {
        return diskManager;
    }

    public DownloadManager getDownloadManager() {
        DownloadManagerState state = diskManager.getDownloadState();

        if (state == null) {
            return (null);
        }

        return (state.getDownloadManager());
    }

    public void dataWritten(long offset, long size) {
        CopyOnWriteList<DiskManagerFileInfoListener> l_ref = listeners;

        if (l_ref != null) {

            for (DiskManagerFileInfoListener listener : l_ref) {

                try {
                    listener.dataWritten(offset, size);

                } catch (Throwable e) {

                    Debug.printStackTrace(e);
                }
            }
        }
    }

    public void dataChecked(long offset, long size) {
        CopyOnWriteList<DiskManagerFileInfoListener> l_ref = listeners;

        if (l_ref != null) {

            for (DiskManagerFileInfoListener listener : l_ref) {

                try {
                    listener.dataChecked(offset, size);

                } catch (Throwable e) {

                    Debug.printStackTrace(e);
                }
            }
        }
    }

    public DirectByteBuffer read(long offset, int length)

    throws IOException {
        DirectByteBuffer buffer = DirectByteBufferPool.getBuffer(DirectByteBuffer.AL_DM_READ, length);

        try {
            cache_file.read(buffer, offset, CacheFile.CP_READ_CACHE);

        } catch (Throwable e) {

            buffer.returnToPool();

            Debug.printStackTrace(e);

            throw (new IOException(e.getMessage()));
        }

        return (buffer);
    }

    private volatile LazyMovingImmediateAverageState read_average_state;
    private volatile LazyMovingImmediateAverageState write_average_state;
    private volatile LazyMovingImmediateAverageState eta_average_state;

    private static LazyMovingImmediateAverageAdapter<DiskManagerFileInfoImpl> read_adapter =
            new LazyMovingImmediateAverageAdapter<DiskManagerFileInfoImpl>() {
                public LazyMovingImmediateAverageState getCurrent(DiskManagerFileInfoImpl instance) {
                    return (instance.read_average_state);
                }

                public void setCurrent(DiskManagerFileInfoImpl instance, LazyMovingImmediateAverageState average) {
                    instance.read_average_state = average;
                }

                public long getValue(DiskManagerFileInfoImpl instance) {
                    return (instance.cache_file.getSessionBytesRead());
                }
            };

    private static LazyMovingImmediateAverageAdapter<DiskManagerFileInfoImpl> write_adapter =
            new LazyMovingImmediateAverageAdapter<DiskManagerFileInfoImpl>() {
                public LazyMovingImmediateAverageState getCurrent(DiskManagerFileInfoImpl instance) {
                    return (instance.write_average_state);
                }

                public void setCurrent(DiskManagerFileInfoImpl instance, LazyMovingImmediateAverageState average) {
                    instance.write_average_state = average;
                }

                public long getValue(DiskManagerFileInfoImpl instance) {
                    return (instance.cache_file.getSessionBytesWritten());
                }
            };

    private static LazyMovingImmediateAverageAdapter<DiskManagerFileInfoImpl> eta_adapter =
            new LazyMovingImmediateAverageAdapter<DiskManagerFileInfoImpl>() {
                public LazyMovingImmediateAverageState getCurrent(DiskManagerFileInfoImpl instance) {
                    return (instance.eta_average_state);
                }

                public void setCurrent(DiskManagerFileInfoImpl instance, LazyMovingImmediateAverageState average) {
                    instance.eta_average_state = average;
                }

                public long getValue(DiskManagerFileInfoImpl instance) {
                    return (instance.cache_file.getSessionBytesWritten());
                }
            };

    public int getReadBytesPerSecond() {
        return ((int) AverageFactory.LazyMovingImmediateAverage(10, 1, read_adapter, this));
    }

    public int getWriteBytesPerSecond() {
        return ((int) AverageFactory.LazyMovingImmediateAverage(10, 1, write_adapter, this));
    }

    public long getETA() {
        if (isSkipped()) {

            return (-1);
        }

        long rem = getLength() - getDownloaded();

        if (rem == 0) {

            return (-1);
        }

        long speed = AverageFactory.LazySmoothMovingImmediateAverage(eta_adapter, this);

        if (speed == 0) {

            return (Constants.CRAPPY_INFINITE_AS_LONG);

        } else {

            long eta = rem / speed;

            if (eta == 0) {

                return (1);

            } else {

                return (eta);
            }
        }
    }

    public void close() {
        // this doesn't need to do anything as overall closure is handled by the disk manager closing
    }

    public void addListener(final DiskManagerFileInfoListener listener) {
        synchronized (this) {

            if (listeners == null) {

                listeners = new CopyOnWriteList<DiskManagerFileInfoListener>();
            }
        }

        if (!listeners.addIfNotPresent(listener)) {

            return;
        }

        new Runnable() {
            private long file_start;
            private long file_end;

            private long current_write_start = -1;
            private long current_write_end = -1;
            private long current_check_start = -1;
            private long current_check_end = -1;

            public void run() {
                TOTorrentFile[] tfs = torrent_file.getTorrent().getFiles();

                long torrent_offset = 0;

                for (int i = 0; i < file_index; i++) {

                    torrent_offset += tfs[i].getLength();
                }

                file_start = torrent_offset;
                file_end = file_start + torrent_file.getLength();

                DiskManagerPiece[] pieces = diskManager.getPieces();

                int first_piece = getFirstPieceNumber();
                int last_piece = getLastPieceNumber();
                long piece_size = torrent_file.getTorrent().getPieceLength();

                for (int i = first_piece; i <= last_piece; i++) {

                    long piece_offset = piece_size * i;

                    DiskManagerPiece piece = pieces[i];

                    if (piece.isDone()) {

                        long bit_start = piece_offset;
                        long bit_end = bit_start + piece.getLength();

                        bitWritten(bit_start, bit_end, true);

                    } else {

                        int block_offset = 0;

                        for (int j = 0; j < piece.getNbBlocks(); j++) {

                            int block_size = piece.getBlockSize(j);

                            if (piece.isWritten(j)) {

                                long bit_start = piece_offset + block_offset;
                                long bit_end = bit_start + block_size;

                                bitWritten(bit_start, bit_end, false);
                            }

                            block_offset += block_size;
                        }
                    }
                }

                bitWritten(-1, -1, false);
            }

            protected void bitWritten(long bit_start, long bit_end, boolean checked) {
                if (current_write_start == -1) {

                    current_write_start = bit_start;
                    current_write_end = bit_end;

                } else if (current_write_end == bit_start) {

                    current_write_end = bit_end;

                } else {

                    if (current_write_start < file_start) {

                        current_write_start = file_start;
                    }

                    if (current_write_end > file_end) {

                        current_write_end = file_end;
                    }

                    if (current_write_start < current_write_end) {

                        try {
                            listener.dataWritten(current_write_start - file_start, current_write_end - current_write_start);

                        } catch (Throwable e) {

                            Debug.printStackTrace(e);
                        }
                    }

                    current_write_start = bit_start;
                    current_write_end = bit_end;
                }

                // checked case

                if (checked && current_check_start == -1) {

                    current_check_start = bit_start;
                    current_check_end = bit_end;

                } else if (checked && current_check_end == bit_start) {

                    current_check_end = bit_end;

                } else {

                    if (current_check_start < file_start) {

                        current_check_start = file_start;
                    }

                    if (current_check_end > file_end) {

                        current_check_end = file_end;
                    }

                    if (current_check_start < current_check_end) {

                        try {
                            listener.dataChecked(current_check_start - file_start, current_check_end - current_check_start);

                        } catch (Throwable e) {

                            Debug.printStackTrace(e);
                        }
                    }

                    if (checked) {
                        current_check_start = bit_start;
                        current_check_end = bit_end;
                    } else {
                        current_check_start = -1;
                        current_check_end = -1;
                    }
                }
            }
        }.run();
    }

    public void removeListener(DiskManagerFileInfoListener listener) {
        synchronized (this) {

            if (listeners != null) {

                listeners.remove(listener);
            }
        }
    }
}
